"""
{prog} - run {solver} on <instance> with the given configuration parameters,
maximum cpu time, and rng seed. Prints the solver's initial configuration and
displays solution metadata each time the upper bound is improved.

Usage:
    {prog} [-p <param>]... [-c <cpu>] [-s <seed>] <instance>
    {prog} (-i | --interactive)
    {prog} (-h | --help | --help-params)
    {prog} (-v | --version)

Arguments:
    <instance>  path to the instance file being solved

Options:
    -h --help                    show help message
    -v --version                 show program version
    -i, --interactive            start an interactive session
    -s <seed>, --seed=<seed>     specify RNG seed
    -c <cpu>, --cpu=<cpu>        cpu time budget  [default: 60.0]
    -p <param>, --param=<param>  specify a solver parameter in the format
                                    'name{param_sep}value'
    --help-params                help about solver parameters
"""
from utils.schema import Or, And, Cut, Use, Optional, Schema, SchemaError
from docopt import docopt
import inspect
import sys
import os
import re
import __main__


PARAM_SEP = ":"
PARAM_RE = re.compile(r"""^\s*
                      (?P<quote>['"]?)
                      (?P<name>\S*)
                      \s*{param_sep}\s*
                      (?P<value>\S*)
                      (?P=quote)
                      \s*$""".format(param_sep=PARAM_SEP),
                      re.VERBOSE)
OPTIONS_SCHEMA = Schema({"<instance>": Or(os.path.isfile, Cut(error="Instance file not found.")),
                         "--seed": Or(None, Use(int, error="Seed should be an integer.")),
                         "--cpu": Or(And(Use(float), lambda cpu: cpu >= 0.0),
                                     Cut(error="CPU should be a non-negative number")),
                         "--param": [And(Use(PARAM_RE.match),
                                         Or(lambda match: match is not None,
                                            Cut(error=("Invalid parameter specification: should"
                                                       " be 'name{}value'".format(PARAM_SEP)))),
                                         Use(lambda match: match.groupdict()))],
                         Optional(str): object})


def solver_cli(solver, argv=sys.argv, prog=None):
    if prog is None:
        prog = os.path.basename(inspect.getfile(__main__))
    solver_name = type(solver).__name__
    doc = __doc__.format(prog=prog, solver=solver_name, param_sep=PARAM_SEP)
    version = "%s version %s" % (solver_name, type(solver).version)
    options = docopt(doc, argv=argv[1:], help=True, version=version, options_first=False)

    # show parameter help message
    if options["--help-params"]:
        solver.params.help()
        return 0

    # start an interactive session
    if options["--interactive"]:
        solver.interactive_session()
        return 0

    # validate the options
    try:
        options = OPTIONS_SCHEMA.validate(options)
    except SchemaError as error:
        print "{}: {}".format(type(error).__name__, error)
        return 1

    instance = options["<instance>"]
    seed = options["--seed"]
    cpu = options["--cpu"]
    params = {param["name"]: param["value"] for param in options["--param"]}

    # initialize the solver and run the search
    if solver.state != solver.STATE.UNINITIALIZED:
        solver.reset()
    solver.init(instance=instance, seed=seed, **params)
    solver.run(cpu=cpu)
    solver.finish()
    return 0


if __name__ == "__main__":
    print docopt(__doc__.format(prog=os.path.basename(__file__),
                                solver="solver_cli",
                                param_sep=PARAM_SEP))
